import logging
import os
from textwrap import wrap

from docutils.parsers.rst import Directive, directives
from docutils import nodes
from nose.importer import add_path
from nose.util import resolve_name

from trestle.example import Example, ShellExample


log = logging.getLogger(__name__)


def suite_directive_factory(suite, cls):
    # create inline subclass bound to the calling suite; this
    # binds the calling suite to each directive class/instance
    class Scls(cls):
        _suite = suite
    return Scls


class SuiteDirective(Directive):
    _suite = None

    @property
    def suite(self):
        return self._suite


class FixtureDirective(SuiteDirective):
    """Directive that loads the fixture module and sets suite.fixture to that
    module.
    """
    required_arguments = 1

    def run(self):
        name = str(self.arguments[0]).strip()
        # Add file dir to paths before attempting to load the
        # fixture
        if self.suite:
            add_path(os.path.dirname(self.suite.filename))
            self.suite.fixture = resolve_name(name)
            log.debug('called %s fixt %s', name, self.suite.fixture)
        return []


class RequestDirective(SuiteDirective):
    """Directive that reads a request section and creates
    a new Example. The example is appended to suite.examples.

    The request directive should include a useful description on the same line
    as the directive. This will become the test name when using verbose
    output. If the request requires custom fixtures, include these as options
    following the directive line and preceding the body of the directive. The
    fixtures will be evaluated in the context of the fixture module assigned
    by the ``.. fixtures`` directive.

    In the body of the directive, you may specify request headers, the request
    line, and any data to be sent with the request, in that order. If you do
    include headers in the request, separate the headers from the request line
    with a blank line,

    Examples:

    .. request :: A simple request

       GET /
    ..

    .. request :: Request with fixtures
       :setup: run_this()
       :teardown: run_that()

       GET /something
    ..

    .. request :: A request with POST data and headers

       Host: www.foobar.com
       Accept: text/xml

       POST /baz
       a=1&b=2
    ..

    Remember to indent the body, and always end with '..' alone on a line, to
    avoid parse errors.
    """
    required_arguments = 1
    final_argument_whitespace = True
    has_content = True
    option_spec = {'setup': directives.unchanged,
                   'teardown': directives.unchanged}

    def run(self):
        description = self.arguments[0]
        if not self.content:
            error = self.state_machine.reporter.error(
                "request %s has no content; is there a space after the last "
                "argument, before the method and url?" % description)
            return [error]
        method, url = self.content.pop(0).split(' ')

        # FIXME look for blank line in content to split out headers

        # allow trailing backslashes on url line (makes
        # long urls more readable)
        while self.content and url.endswith('\\'):
            url = url[:-1] + self.content.pop(0).strip()

        # FIXME allow override of default encoding
        text = '\n'.join(self.content).encode('utf-8')
        if self.content:
            data = text
        else:
            data = None
        if self.suite:
            ex = Example(self.suite.filename, description, method, url, data,
                         setup=self.options.get('setup'),
                         teardown=self.options.get('teardown'))
            self.suite.examples.append(ex)
        call = "%s %s" % (method, url)
        out = nodes.literal_block(
            '', '',
            nodes.literal(method, method, classes=['request', 'method']),
            nodes.Text(' '),
            nodes.literal(url, url, classes=['request', 'url']),
            )
        if text:
            # wrap long lines?
            # FIXME wrap size should be configurable
            text = '\n\n' + text
            out += nodes.Text(text, text)
        return [out, nodes.caption(description, description)]


class ResponseDirective(SuiteDirective):
    """Directive that adds an expected response to the current
    example.

    Use this directive to add an expected response to match against the
    request most recently defined in the test document.

    You may match against response headers and content, or content only. To
    match against both headers and content, separate the headers from the
    content with a blank line.

    Examples:

    .. response ::

       200 OK
       Content-Type: text/plain
       ...

       Hello
    ..

    .. response ::

       This is just content
    ..
    """
    has_content = True

    def run(self):
        if self.suite:
            ex = self.suite.examples[-1]
            ex.want.append(self.content)

        # FIXME break long lines
        text = '\n'.join(self.content)
        node = nodes.literal_block(text, text, classes=['response'])
        if self.suite:
            node.example = ex
        return [node]


class ShellDirective(SuiteDirective):
    """
    Directive that creates a ShellExample. Format the directive with the
    shell command on the same line as the directive and the expected stdout
    output in the body of the directive:

    .. shell :: echo "Hello"

      Hello
    ..

    Remember to indent the body, and always end with '..' alone on a line, to
    avoid parse errors.
    """
    has_content = True
    required_arguments = 1
    final_argument_whitespace = True

    option_spec = {'setup': directives.unchanged,
                   'teardown': directives.unchanged,
                   'cwd': directives.unchanged,
                   'post': directives.unchanged,
                   'stderr': directives.flag}
    def run(self):
        command = self.arguments[0]
        log.debug('shell command %s', command)
        if self.options.get('cwd'):
            log.debug('command will run in dir %s', self.options.get('cwd'))
        if not self.content:
            error = self.state_machine.reporter.error(
                "shell %s has no expected output; "
                "is there a space after the last "
                "argument, before the method and url?" % command)
            return [error]

        # FIXME allow overriding default encoding
        expect = '\n'.join(self.content).encode('utf-8')
        log.debug('command expects %s', expect)

        if self.options.get('post') and self.suite:
            post = eval(self.options.get('post'), self.suite.fixture.__dict__)
        else:
            post = None

        if self.suite:
            ex = ShellExample(self.suite.filename, command, expect,
                              setup=self.options.get('setup'),
                              teardown=self.options.get('teardown'),
                              cwd=self.options.get('cwd'),
                              post=post,
                              stderr=('stderr' in self.options))
            self.suite.examples.append(ex)

        out = nodes.literal_block(
            '', '',
            nodes.literal(command, command, classes=['request', 'method'])
            )
        response = nodes.literal_block(expect, expect, classes=['response'])
        if self.suite:
            response.example = ex
        return [out, response]
